/*
 * Copyright (c) laokz <laokz@foxmail.com>
 * VMWIFI is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *          http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
#include <netlink/netlink.h>
#include <netlink/genl/genl.h>
#include <netlink/genl/ctrl.h>
#include <linux/nl80211.h>
#include "vmwifi.h"
#include "vw.h"

/* store parameters used for communication with host wlan */
struct TyHostParam {
    struct nl_sock *sk; /* netlink socket */
    int family;         /* nl80211 family ID */
    int scanGrp;        /* multicast "scan" group ID */
    int idx;            /* host wlan interface index */
    char name[IFNAMSIZ];
};
static struct TyHostParam g_wlanPara;

/* result write to g_wlanPara */
static int CbGetIfIdxName(struct nl_msg *msg, void *arg)
{
    struct nlattr *tb[NUM_NL80211_ATTR] = { 0 };
    int ret;
    struct nlmsghdr *nlh = nlmsg_hdr(msg);

    if (genlmsg_hdr(nlh)->cmd != NL80211_CMD_NEW_INTERFACE) {
        return NL_SKIP;
    }

    ret = genlmsg_parse(nlh, 0, tb, NL80211_ATTR_MAX, NULL);
    NL_OUT_IF_ERR(ret);

    g_wlanPara.idx = nla_get_u32(tb[NL80211_ATTR_IFINDEX]);
    strncpy(g_wlanPara.name, nla_get_string(tb[NL80211_ATTR_IFNAME]), IFNAMSIZ - 1);

OUT:    /* NL_OK to consume the remaining message */
    return NL_OK;
}

/*
 * If multiple cards exist, the last one returned(?).
 * Resolve nl80211 family, store to static.
 * Function doesn't pollute nl_sock callbacks.
 */
static int GetWlanIfIdxName()
{
    struct nl_cb *cb = NULL;
    int ret;

    g_wlanPara.family = genl_ctrl_resolve(g_wlanPara.sk, NL80211_GENL_NAME);
    NL_OUT_IF_ERR(g_wlanPara.family);

    cb = nl_cb_alloc(NL_CB_CUSTOM);
    OUT_IF_ERR_PTR(cb);
    nl_cb_set(cb, NL_CB_VALID, NL_CB_CUSTOM, CbGetIfIdxName, NULL);

    ret = genl_send_simple(g_wlanPara.sk, g_wlanPara.family, NL80211_CMD_GET_INTERFACE, 0, NLM_F_DUMP);
    NL_OUT_IF_ERR(ret);

    ret = nl_recvmsgs(g_wlanPara.sk, cb);
    NL_OUT_IF_ERR(ret);

OUT:
    if (cb) {
        nl_cb_put(cb);
    }
    return ret;
}

static int CbWaitScan(struct nl_msg *msg, void *arg)
{
    struct genlmsghdr *gnlh = genlmsg_hdr(nlmsg_hdr(msg));
    int *flag = arg;

    if (gnlh->cmd == NL80211_CMD_SCAN_ABORTED ||
        gnlh->cmd == NL80211_CMD_NEW_SCAN_RESULTS) {
        *flag = 1;
    }

    return NL_SKIP;
}

/* trigger scan and wait completion */
static int TriggerScan(void)
{
    struct nl_msg *msg = NULL;
    int ret, flag = 0;
    void *ptr = NULL;

    nl_socket_modify_cb(g_wlanPara.sk, NL_CB_VALID, NL_CB_CUSTOM, CbWaitScan, &flag);
    nl_socket_disable_seq_check(g_wlanPara.sk);    /* avoid unwanted broken */

    msg = nlmsg_alloc();
    OUT_IF_ERR_PTR(msg);
    ptr = genlmsg_put(msg, NL_AUTO_PORT, NL_AUTO_SEQ, g_wlanPara.family, 0, 0, NL80211_CMD_TRIGGER_SCAN, 0);
    OUT_IF_ERR_PTR(ptr);
    NLA_PUT_U32(msg, NL80211_ATTR_IFINDEX, g_wlanPara.idx);

    ret = nl_send_auto(g_wlanPara.sk, msg);
    NL_OUT_IF_ERR(ret);

    while (flag == 0) {
        ret = nl_recvmsgs_default(g_wlanPara.sk);
        NL_OUT_IF_ERR(ret);
    }

nla_put_failure:    /* NLA_PUT_xxx need this label */
OUT:
    if (msg) {
        nlmsg_free(msg);
    }               /* restore socket callbacks */
    nl_socket_modify_cb(g_wlanPara.sk, NL_CB_SEQ_CHECK, NL_CB_DEFAULT, NULL, NULL);
    nl_socket_modify_cb(g_wlanPara.sk, NL_CB_VALID, NL_CB_DEFAULT, NULL, NULL);
    return ret;
}

/* get wlan scan result, use `f` send back to guest */
static int GetScanAndResp(nl_recvmsg_msg_cb_t f, void *arg)
{
    struct nl_cb *cb = NULL;
    struct nl_msg *msg = NULL;
    void *ptr = NULL;
    int ret;

    cb = nl_cb_alloc(NL_CB_CUSTOM);
    OUT_IF_ERR_PTR(cb);
    nl_cb_set(cb, NL_CB_VALID, NL_CB_CUSTOM, f, arg);   /* here is the main idea */

    msg = nlmsg_alloc();
    OUT_IF_ERR_PTR(msg);
    ptr = genlmsg_put(msg, NL_AUTO_PORT, NL_AUTO_SEQ, g_wlanPara.family, 0, NLM_F_DUMP, NL80211_CMD_GET_SCAN, 0);
    OUT_IF_ERR_PTR(ptr);
    NLA_PUT_U32(msg, NL80211_ATTR_IFINDEX, g_wlanPara.idx);

    ret = nl_send_auto(g_wlanPara.sk, msg);
    NL_OUT_IF_ERR(ret);

    ret = nl_recvmsgs(g_wlanPara.sk, cb);
    NL_OUT_IF_ERR(ret);

nla_put_failure:
OUT:
    if (msg) {
        nlmsg_free(msg);
    }
    if (cb) {
        nl_cb_put(cb);
    }
    return ret;
}

/* function doesn't pollute nl_sock callbacks */
int VwDoScan(nl_recvmsg_msg_cb_t f, void *arg)
{
    int ret;
    short flag = 0;

    /* subscribe to kernel nl80211 multicast "scan" group */
    if (!g_wlanPara.scanGrp) {
        g_wlanPara.scanGrp = genl_ctrl_resolve_grp(g_wlanPara.sk, NL80211_GENL_NAME,
                                                        NL80211_MULTICAST_GROUP_SCAN);
    }
    NL_OUT_IF_ERR(g_wlanPara.scanGrp);
    ret = nl_socket_add_memberships(g_wlanPara.sk, g_wlanPara.scanGrp, NFNLGRP_NONE);
    NL_OUT_IF_ERR(ret);
    flag = 1;

    ret = TriggerScan();
    OUT_IF__1(ret);
    LOG("trigger scan success\n");

    ret = GetScanAndResp(f, arg);
    OUT_IF__1(ret);
    flag = VW_CODE_DONE;
    VwSendResponse(NULL, (char *)&flag, sizeof(short));
    LOG("respond scan results done\n");

OUT:
    if (flag) {
        nl_socket_drop_memberships(g_wlanPara.sk, g_wlanPara.scanGrp);
    }
    return ret;
}

void VwDeInit(void)
{
    if (g_wlanPara.sk) {
        nl_socket_free(g_wlanPara.sk);
    }
    DeinitVwEther();
}

int VwInit(const char *iface)
{
    int ret;

    ret = InitVwEther(iface);
    OUT_IF__1(ret);

    g_wlanPara.sk = nl_socket_alloc();
    OUT_IF_ERR_PTR(g_wlanPara.sk);

    ret = genl_connect(g_wlanPara.sk);
    NL_OUT_IF_ERR(ret);

    /* nl80211 family also resolved here */
    ret = GetWlanIfIdxName();
    OUT_IF__1(ret);
    LOG("netlink socket init success, ifname:%s, index=%d\n", g_wlanPara.name, g_wlanPara.idx);

    return 0;

OUT:
    VwDeInit();
    return ret;
}
